文档预览
目前实现大致有三种解决方案:
1.使用微软提供的接口。这个方案虽然省事,而且效果是最好的,但是文件 url 必须是公网可访问的,对于局域网部署或者文件保密的情况并不适用(谷歌文档在线预览)
src=”‘https://docs.google.com/viewer?url=url src=’https://view.officeapps.live.com/op/view.aspx?src=url 金山文档在线预览,私有化部署 使用 Java 后端统一转换,在前端预览。该方案兼容性较好,效果仅次于微软提供接口,但是对于服务器的压力比较大,高并发,吞吐量场景不适用 纯前端将文件转换成 html,效果不太理想,文件格式未必能保留,老文件格式 doc 不支持,不消耗服务器性能
前端方案
文档格式 | 开源方案 |
---|---|
word(docx) | mammoth,docx-preview |
excel(xlsx) | exceljs,Luckysheet,handsontable |
powerpoint(pptx) | pptxjs |
pdf(pdf) | pdfjs |
测试路径
前端文件下载
form 表单
兼容性好,不知道下载进度,不能直接下载浏览器直接预览的文件txt,png
/**
* 下载文件
* @param {String} fileName - 文件名
*/
function downloadFile (downloadUrl, fileName) {
// 创建表单
const formObj = document.createElement('form');
formObj.action = downloadUrl;
formObj.method = 'get';
formObj.style.display = 'none';
// 创建input,主要是起传参作用
const formItem = document.createElement('input');
formItem.value = fileName; // 传参的值
formItem.name = 'fileName'; // 传参的字段名
// 插入到网页中
formObj.appendChild(formItem);
document.body.appendChild(formObj);
formObj.submit(); // 发送请求
document.body.removeChild(formObj); // 发送完清除掉
}
open 或 location.href
不能直接下载浏览器直接预览的文件txt,png,不能鉴权,不能查看下载进度,url长度限制
window.open('downloadFile.zip');
location.href = 'downloadFile.zip';
a 标签的 download
不能鉴权,h5新特性,不能下载跨域浏览器可浏览的文件,可下载浏览器直接与预览的文件
<a href="example.jpg" download='test'>点击下载</a>
利用 Blob 对象
兼容性
/**
* 下载文件
* @param {String} path - 下载地址/下载请求地址。
* @param {String} name - 下载文件的名字/重命名(考虑到兼容性问题,最好加上后缀名)
*/
downloadFile (path, name) {
const xhr = new XMLHttpRequest();
xhr.open('get', path);
xhr.responseType = 'blob';
xhr.send();
xhr.onload = function () {
if (this.status === 200 || this.status === 304) {
// 如果是IE10及以上,不支持download属性,采用msSaveOrOpenBlob方法,但是IE10以下也不支持msSaveOrOpenBlob
if ('msSaveOrOpenBlob' in navigator) {
navigator.msSaveOrOpenBlob(this.response, name);
return;
}
// const blob = new Blob([this.response], { type: xhr.getResponseHeader('Content-Type') });
// const url = URL.createObjectURL(blob);
const url = URL.createObjectURL(this.response);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = name;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
};
}
利用 base64
兼容性
/**
* 下载文件
* @param {String} path - 下载地址/下载请求地址。
* @param {String} name - 下载文件的名字(考虑到兼容性问题,最好加上后缀名)
*/
downloadFile (path, name) {
const xhr = new XMLHttpRequest();
xhr.open('get', path);
xhr.responseType = 'blob';
xhr.send();
xhr.onload = function () {
if (this.status === 200 || this.status === 304) {
const fileReader = new FileReader();
fileReader.readAsDataURL(this.response);
fileReader.onload = function () {
const a = document.createElement('a');
a.style.display = 'none';
a.href = this.result;
a.download = name;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
};
}
};
}
文件上传
大文件上传
// 文件二进制流,基础单位字节Byte
const [firstFile] = e.target.files;
// 初始化切片大小10M
const SIZE = 10 * 1024 * 1024
// 生成文件切片 (客户端文件上传事件触发时执行)
createFileChunk(file, size = SIZE) {
const result = [];
let cur = 0;
while (cur < file.size) {
result.push({ fileItem: file.slice(cur, cur + size) });
cur += size;
}
return result
}
// 上传按钮触发时执行
async handleUpload() {
if (!firstFile) return;
const result = createFileChunk(firstFile);
sliceData = result.map(({ fileItem },index) => (
{
chunk: fileItem,
hash: firstFile.name + "-" + index // 文件名 + 数组下标
}
));
await uploadChunks();
}
// 文件上传
async uploadChunks() {
const requestList = sliceData
.map(({ chunk,hash }) => {
const formData = new FormData();
formData.append("chunk", chunk);
formData.append("hash", hash);
formData.append("filename", firstFile.name);
return fetch("http://localhost:3000",{
method:'POST',
body: formData
})
})
// 并发切片
await Promise.all(requestList);
// 前端发送合并切片请求
await fetch("http://localhost:3000/merge",{
method:'POST',
headers: {
"content-type": "application/json"
},
body: JSON.stringify({
filename: firstFile.name
})
})
}
断点续传
断点续传:前端/服务端需要记住已上传的切片
方案1.前端使用缓存 localStorage 记录已上传的切片 hash 方案2.服务端保存已上传的切片 hash,前端每次上传前向服务端获取已上传的切片 在前面使用文件名+切片下标作为hash值,一旦文件名改变就会失效,应该根据内容生成hash,只要内容不变就不会失效 spark-md5:根据文件内容计算出文件的 hash 值,另开一个线程防止堵塞worker 线程计算 hash
hash生成优化
// 根据文件内容生成 hash
// 要计算所有文件切片的 hash 值,否则hash有大概率相同
self.importScripts("/spark-md5.min.js"); // 导入脚本
self.onmessage = e => {
const { fileChunkList } = e.data;
const spark = new self.SparkMD5.ArrayBuffer();
let percentage = 0;
let count = 0;
const loadNext = index => {
const reader = new FileReader();
reader.readAsArrayBuffer(fileChunkList[index].file);
reader.onload = e => {
count++;
spark.append(e.target.result);
if (count === fileChunkList.length) {
self.postMessage({
percentage: 100,
hash: spark.end()
});
self.close();
} else {
percentage += 100 / fileChunkList.length;
self.postMessage({
percentage
});
// 递归计算下一个切片
loadNext(count);
}
};
};
loadNext(0);
};
// 主线程与worker通讯
// 生成文件 hash(web-worker)
calculateHash(fileChunkList) {
return new Promise(resolve => {
// 添加 worker 属性
this.container.worker = new Worker("/hash.js");
this.container.worker.postMessage({ fileChunkList });
this.container.worker.onmessage = e => {
const { percentage, hash } = e.data;
this.hashPercentage = percentage;
if (hash) {
resolve(hash);
}
};
});
},
async handleUpload() {
if (!this.container.file) return;
const fileChunkList = this.createFileChunk(this.container.file);
this.container.hash = await this.calculateHash(fileChunkList);
this.data = fileChunkList.map(({ file },index) => ({
fileHash: this.container.hash,
chunk: file,
hash: this.container.file.name + "-" + index, // 文件名 + 数组下标
percentage:0
}));
await this.uploadChunks();
}
文件暂停
中断请求,并清空正在上传的切片
1.axios
const CancelToken = axios.CancelToken.source()
axios.get(url, {
cancelToken: CancelToken.token
}).catch((thrown)=> {
if (axios.isCancel(thrown)) {
console.log('请求已取消', thrown.message);
}
});
axios.post(url, {
name:"test",
value:'3242'
},{
cancelToken: CancelToken.token
}).catch((thrown)=> {
if (axios.isCancel(thrown)) {
console.log('请求已取消', thrown.message);
}
});
// or
axios({
url:url,
params:{
name: 'new name'
},
cancelToken: CancelToken.token
})
//取消请求
CancelToken.cancel('取消请求!');
2.fetch
let abortController = new AbortController();
fetch(url, { signal: abortController.signal })
abortController.abort()
3.ajax
let ajax = new XMLHttpRequest()
ajax.open('GET/POST',url,false)
ajax.send()
ajax.onreadystatechange = ()=>{
if(ajax.status ===200 && ajax.readyState = 4){
console.log(ajax.response)
}
}
ajax.abort()
文件秒传,续传
文件秒传依赖断点续传生成的 hash,把生成的 hash 发送给服务端进行验证,服务端一旦找到 hash 相同的文件,直接返回上传成功的信息 续传需要向服务器验证已上传的切片,前端过滤
前端每次上传前发送一个验证的请求,返回三种结果 服务端已存在该文件,不需要再次上传 服务端不存在该文件,完全上传资源 服务端存在部分文件,并把已上传的文件切片返回给前端,前端过滤已经上传切片